1 /*
2 Copyright: Marcelo S. N. Mancini (Hipreme|MrcSnm), 2018 - 2021
3 License:   [https://creativecommons.org/licenses/by/4.0/|CC BY-4.0 License].
4 Authors: Marcelo S. N. Mancini
5 
6 	Copyright Marcelo S. N. Mancini 2018 - 2021.
7 Distributed under the CC BY-4.0 License.
8    (See accompanying file LICENSE.txt or copy at
9 	https://creativecommons.org/licenses/by/4.0/
10 */
11 module hip.graphics.g2d.spritebatch;
12 import hip.graphics.mesh;
13 import hip.graphics.orthocamera;
14 import hip.hiprenderer.renderer;
15 import hip.assets.texture;
16 import hip.hiprenderer.framebuffer;
17 import hip.error.handler;
18 import hip.hiprenderer.shader;
19 public import hip.api.graphics.batch;
20 public import hip.api.graphics.color;
21 public import hip.math.vector;
22 public import hip.math.matrix;
23 
24 /**
25 *   This is what to expect in each vertex sent to the sprite batch
26 */
27 @HipShaderInputLayout struct HipSpriteVertex
28 {
29     Vector3 vPosition = Vector3.zero;
30     HipColor vColor = HipColor.white;
31     Vector2 vTexST = Vector2.zero;
32     float vTexID = 0;
33 
34     static enum floatCount = cast(size_t)(HipSpriteVertex.sizeof/float.sizeof);
35     static enum quadCount = floatCount*4;
36     // static assert(HipSpriteVertex.floatCount == 10,  "SpriteVertex should contain 9 floats and 1 int");
37 }
38 
39 @HipShaderVertexUniform("Cbuf1")
40 struct HipSpriteVertexUniform
41 {
42     Matrix4 uModel = Matrix4.identity;
43     Matrix4 uView = Matrix4.identity;
44     Matrix4 uProj = Matrix4.identity;
45 }
46 
47 @HipShaderFragmentUniform("Cbuf")
48 struct HipSpriteFragmentUniform
49 {
50     float[4] uBatchColor = [1,1,1,1];
51     
52     @(ShaderHint.Blackbox | ShaderHint.MaxTextures) 
53     IHipTexture[] uTex;
54 }
55 
56 /**
57 *   The spritebatch contains 2 shaders.
58 *   One shader is entirely internal, which you don't have any control, this is for actually being able
59 *   to draw stuff on the screen.
60 *
61 *   The another one is a post processing shader, which the spritebatch doesn't uses by default. If 
62 *   setPostProcessingShader()
63 */
64 class HipSpriteBatch : IHipBatch
65 {
66     index_t maxQuads;
67     index_t[] indices;
68     HipSpriteVertex[] vertices;
69 
70     protected bool hasInitTextureSlots;
71     protected Shader spriteBatchShader;
72 
73     ///Post Processing Shader
74     protected Shader ppShader;
75     protected HipFrameBuffer fb;
76     protected HipTextureRegion fbTexRegion;
77     protected float managedDepth = 0;
78 
79     HipOrthoCamera camera;
80     Mesh mesh;
81 
82     protected IHipTexture[] currentTextures;
83     int usingTexturesCount;
84 
85     uint lastDrawQuadsCount = 0;
86     uint quadsCount;
87 
88 
89     this(HipOrthoCamera camera = null, index_t maxQuads = 10_900)
90     {
91         import hip.util.conv:to;
92         ErrorHandler.assertLazyExit(index_t.max > maxQuads * 6, "Invalid max quads. Max is "~to!string(index_t.max/6));
93         this.maxQuads = maxQuads;
94         indices = new index_t[maxQuads*6];
95         vertices = new HipSpriteVertex[maxQuads]; //XYZ -> 3, RGBA -> 4, ST -> 2, TexID 3+4+2+1=10
96         vertices[] = HipSpriteVertex.init;
97         currentTextures = new IHipTexture[](HipRenderer.getMaxSupportedShaderTextures());
98         usingTexturesCount = 0;
99 
100         this.spriteBatchShader = HipRenderer.newShader(HipShaderPresets.SPRITE_BATCH);
101         spriteBatchShader.addVarLayout(ShaderVariablesLayout.from!HipSpriteVertexUniform);
102         spriteBatchShader.addVarLayout(ShaderVariablesLayout.from!HipSpriteFragmentUniform);
103         spriteBatchShader.setBlending(HipBlendFunction.SRC_ALPHA, HipBlendFunction.ONE_MINUS_SRC_ALPHA, HipBlendEquation.ADD);
104 
105         mesh = new Mesh(HipVertexArrayObject.getVAO!HipSpriteVertex, spriteBatchShader);
106         mesh.vao.bind();
107         mesh.createVertexBuffer(cast(index_t)(maxQuads*HipSpriteVertex.quadCount), HipBufferUsage.DYNAMIC);
108         mesh.createIndexBuffer(cast(index_t)(maxQuads*6), HipBufferUsage.STATIC);
109 
110         
111 
112         spriteBatchShader.useLayout.Cbuf;
113         // spriteBatchShader.bind();
114         // spriteBatchShader.sendVars();
115 
116         mesh.sendAttributes();
117         
118 
119         spriteBatchShader.useLayout.Cbuf;
120         spriteBatchShader.bind();
121         spriteBatchShader.sendVars();
122 
123         if(camera is null)
124             camera = new HipOrthoCamera();
125         this.camera = camera;
126         HipVertexArrayObject.putQuadBatchIndices(indices, maxQuads);
127         mesh.setVertices(vertices);
128         mesh.setIndices(indices);
129         setTexture(HipTexture.getPixelTexture());
130     }
131     void setCurrentDepth(float depth){managedDepth = depth;}
132 
133     void setShader(Shader s)
134     {
135         if(fb is null)
136         {
137             Viewport v = HipRenderer.getCurrentViewport;
138             fb = HipRenderer.newFrameBuffer(cast(int)v.width, cast(int)v.height);
139             // fbTexRegion = new HipTextureRegion(fb.getTexture());
140         }
141         this.ppShader = s;
142     }
143 
144     /**
145     *   Sets the texture slot/index for the current quad and points it to the next quad
146     */
147     void addQuad(void[] quad, int slot)
148     {
149         if(quadsCount+1 > maxQuads)
150             flush();
151 
152         size_t start = quadsCount;
153         version(none) //D way to do it, but it is also slower
154         {
155             size_t end = start + HipSpriteVertex.quadCount;
156             vertices[start..end] = quad;
157             vertices[start+ T1] = slot;
158             vertices[start+ T2] = slot;
159             vertices[start+ T3] = slot;
160             vertices[start+ T4] = slot;
161         }
162         else
163         {
164             import core.stdc.string;
165             HipSpriteVertex* v = cast(HipSpriteVertex*)vertices.ptr;
166             memcpy(v + start, quad.ptr, HipSpriteVertex.sizeof * 4);
167             v[0].vTexID = slot;
168             v[1].vTexID = slot;
169             v[2].vTexID = slot;
170             v[3].vTexID = slot;
171         }
172         
173         quadsCount++;
174     }
175 
176     void addQuads(void[] quadsVertices, int slot)
177     {
178         import hip.util.array:swapAt;
179         assert(quadsVertices.length % (HipSpriteVertex.sizeof*4) == 0, "Count must be divisible by HipSpriteVertex.sizeof*4");
180         HipSpriteVertex[] v = cast(HipSpriteVertex[])quadsVertices;
181         uint countOfQuads = cast(uint)(v.length / 4);
182 
183 
184         while(countOfQuads > 0)
185         {
186             size_t remainingQuads = this.maxQuads - this.quadsCount;
187             if(remainingQuads == 0)
188             {
189                 flush();
190                 this.usingTexturesCount = 1;
191                 swapAt(this.currentTextures, 0, slot);//Guarantee the target slot is being used
192                 remainingQuads = this.maxQuads;
193             }
194             size_t quadsToDraw = (countOfQuads < remainingQuads) ? countOfQuads : remainingQuads;
195 
196             size_t start = quadsCount;
197             size_t end = (start + quadsToDraw)*4;
198 
199             vertices[start..end] = v;
200             for(int i = 0; i < quadsToDraw; i++)
201             {
202                 vertices[start + i].vTexID = slot;
203                 vertices[start + i+1].vTexID = slot;
204                 vertices[start + i+2].vTexID = slot;
205                 vertices[start + i+3].vTexID = slot;
206             }
207             v = v[quadsToDraw..$];
208 
209             if(quadsToDraw + remainingQuads == maxQuads)
210             {
211                 flush();
212                 this.usingTexturesCount = 1;
213                 swapAt(this.currentTextures, 0, slot);//Guarantee the target slot is being used
214             }
215             else
216                 this.quadsCount+= quadsToDraw;
217             countOfQuads-= quadsToDraw;
218         }
219     }
220     
221     private int getNextTextureID(IHipTexture t)
222     {
223         for(int i = 0; i < usingTexturesCount; i++)
224             if(currentTextures[i] is t)
225                 return i;
226         if(usingTexturesCount < currentTextures.length)
227         {
228             currentTextures[usingTexturesCount] = t;
229             return usingTexturesCount++;            
230         }
231         return -1;
232     }
233     /**
234     *   Sets the current texture in use on the sprite batch and returns its slot.
235     */
236     protected int setTexture (IHipTexture texture)
237     {
238         int slot = getNextTextureID(texture);
239         if(slot == -1)
240         {
241             flush();
242             slot = getNextTextureID(texture);
243         }
244         return slot;
245     }
246     protected int setTexture(IHipTextureRegion reg){return setTexture(reg.getTexture());}
247 
248     protected static bool isZeroAlpha(void[] vertices)
249     {
250         HipSpriteVertex[] v = cast(HipSpriteVertex[])vertices;
251         return v[0].vColor.a == 0 && v[1].vColor.a == 0 && v[2].vColor.a == 0 && v[3].vColor.a == 0;
252     }
253 
254     void draw(IHipTexture t, ubyte[] vertices)
255     {
256         if(isZeroAlpha(vertices)) return;
257         ErrorHandler.assertExit(t.getWidth != 0 && t.getHeight != 0, "Tried to draw 0 bounds sprite");
258         int slot = setTexture(t);
259         ErrorHandler.assertExit(slot != -1, "HipTexture slot can't be -1 on draw phase");
260 
261         if(vertices.length == HipSpriteVertex.quadCount)
262             addQuad(vertices, slot);
263         else
264             addQuads(vertices, slot);
265     }
266 
267     void draw(IHipTexture texture, int x, int y, int z = 0, in HipColor color = HipColor.white, float scaleX = 1, float scaleY = 1, float rotation = 0)
268     {
269         import hip.global.gamedef;
270         if(color.a == 0) return;
271         if(quadsCount+1 > maxQuads)
272             flush();
273         if(texture is null)
274             texture = cast()getDefaultTexture();
275         ErrorHandler.assertExit(texture.getWidth() != 0 && texture.getHeight() != 0, "Tried to draw 0 bounds texture");
276         int slot = setTexture(texture);
277         ErrorHandler.assertExit(slot != -1, "HipTexture slot can't be -1 on draw phase");
278 
279         size_t startVertex = quadsCount *4;
280         size_t endVertex = startVertex + 4;
281 
282         getTextureVertices(vertices[startVertex..endVertex], slot, texture,x,y,managedDepth,color, scaleX, scaleY, rotation);
283         quadsCount++;
284     }
285 
286 
287     void draw(IHipTextureRegion reg, int x, int y, int z = 0, in HipColor color = HipColor.white, float scaleX = 1, float scaleY = 1, float rotation = 0)
288     {
289         if(color.a == 0) return;
290         if(quadsCount+1 > maxQuads)
291             flush();
292         ErrorHandler.assertExit(reg.getWidth() != 0 && reg.getHeight() != 0, "Tried to draw 0 bounds region");
293         int slot = setTexture(reg);
294         ErrorHandler.assertExit(slot != -1, "HipTexture slot can't be -1 on draw phase");
295         size_t startVertex = quadsCount*4;
296         size_t endVertex = startVertex + 4;
297 
298         getTextureRegionVertices(vertices[startVertex..endVertex], slot, reg,x,y,managedDepth,color, scaleX, scaleY, rotation);
299         quadsCount++;
300     }
301 
302     private static void setColor(HipSpriteVertex[] ret, in HipColor color)
303     {
304         ret[0].vColor = color;
305         ret[1].vColor = color;
306         ret[2].vColor = color;
307         ret[3].vColor = color;
308     }
309 
310     private static void setZ(HipSpriteVertex[] vertices, float z)
311     {
312         vertices[0].vPosition.z = z;
313         vertices[1].vPosition.z = z;
314         vertices[2].vPosition.z = z;
315         vertices[3].vPosition.z = z;
316     }
317     private static void setUV(HipSpriteVertex[] vertices, const scope ref float[8] uv)
318     {
319         vertices[0].vTexST = Vector2(uv[0], uv[1]);
320         vertices[1].vTexST = Vector2(uv[2], uv[3]);
321         vertices[2].vTexST = Vector2(uv[4], uv[5]);
322         vertices[3].vTexST = Vector2(uv[6], uv[7]);
323     }
324     private static void setTID(HipSpriteVertex[] vertices, int tid)
325     {
326         vertices[0].vTexID = tid;
327         vertices[1].vTexID = tid;
328         vertices[2].vTexID = tid;
329         vertices[3].vTexID = tid;
330     }
331     private static void setBounds(HipSpriteVertex[] vertices, float x, float y, float width, float height, float scaleX = 1, float scaleY = 1)
332     {
333         width*= scaleX;
334         height*= scaleY;
335         vertices[0].vPosition.xy = Vector2(x, y);
336         vertices[1].vPosition.xy = Vector2(x+width, y);
337         vertices[2].vPosition.xy = Vector2(x+width, y+height);
338         vertices[3].vPosition.xy = Vector2(x, y+height);
339     }
340 
341     private static void setBoundsFromRotation(HipSpriteVertex[] vertices, float x, float y, float width, float height, float rotation, float scaleX = 1, float scaleY = 1)
342     {
343         import hip.math.utils:cos,sin;
344         width*= scaleX;
345         height*= scaleY;
346         float centerX = -width/2;
347         float centerY = -height/2;
348         float x2 = x + width;
349         float y2 = y + height;
350         float c = cos(rotation);
351         float s = sin(rotation);
352 
353         vertices[0].vPosition.xy = Vector2(c*centerX - s*centerY + x, c*centerY + s*centerX + y);
354         vertices[1].vPosition.xy = Vector2(c*x2 - s*centerY + x, c*centerY + s*x2 + y);
355         vertices[2].vPosition.xy = Vector2(c*x2 - s*y2 + x, c*y2 + s*x2 + y);
356         vertices[3].vPosition.xy = Vector2(c*centerX - s*y2 + x, c*y2 + s*centerX + y);
357     }
358 
359 
360     static void getTextureVertices(HipSpriteVertex[] output, int slot, IHipTexture texture,
361     int x, int y, float z = 0, in HipColor color = HipColor.white, float scaleX = 1, float scaleY = 1, float rotation = 0)
362     {
363         int width = texture.getWidth();
364         int height = texture.getHeight();
365 
366         const float[8] v = HipTextureRegion.defaultVertices;
367         setUV(output, v);
368         setZ(output, z);
369         setTID(output, slot);
370         setColor(output, color);
371         if(rotation == 0)
372             setBounds(output, x, y, width, height, scaleX, scaleY);
373         else
374             setBoundsFromRotation(output, x, y, width, height, rotation, scaleX, scaleY);
375     }
376 
377     static void getTextureRegionVertices(HipSpriteVertex[] output, int slot, IHipTextureRegion reg,
378     int x, int y, float z = 0, in HipColor color = HipColor.white, float scaleX = 1, float scaleY = 1, float rotation = 0)
379     {
380         int width = reg.getWidth();
381         int height = reg.getHeight();
382         setZ(output, z);
383         setColor(output, color);
384         setTID(output, slot);
385         setUV(output, reg.getVertices());
386         if(rotation == 0)
387             setBounds(output, x, y, width, height, scaleX, scaleY);
388         else
389             setBoundsFromRotation(output, x, y, width, height, rotation, scaleX, scaleY);
390     }
391 
392     
393 
394     void draw()
395     {
396         if(quadsCount - lastDrawQuadsCount != 0)
397         {
398             for(int i = usingTexturesCount; i < currentTextures.length; i++)
399                 currentTextures[i] = currentTextures[0];
400             mesh.bind();
401 
402             mesh.shader.setVertexVar("Cbuf1.uProj", camera.proj, false);
403             mesh.shader.setVertexVar("Cbuf1.uModel",Matrix4.identity(), false);
404             mesh.shader.setVertexVar("Cbuf1.uView", camera.view, false);
405             mesh.shader.setFragmentVar("Cbuf.uTex", currentTextures);
406             mesh.shader.bindArrayOfTextures(currentTextures, "uTex");
407             mesh.shader.sendVars();
408 
409             size_t start = lastDrawQuadsCount*4;
410             size_t end = quadsCount*4;
411             mesh.updateVertices(cast(void[])vertices[start..end],cast(int)start);
412             mesh.draw((quadsCount-lastDrawQuadsCount)*6, HipRendererMode.TRIANGLES, lastDrawQuadsCount*6);
413 
414             ///Some operations may require texture unbinding(D3D11 Framebuffer)
415             foreach(i; 0..usingTexturesCount)
416                 currentTextures[i].unbind(i);
417             mesh.unbind();
418         }
419         lastDrawQuadsCount = quadsCount;
420     }
421 
422     void flush()
423     {
424         if(ppShader !is null)
425             fb.bind();
426         draw();
427         lastDrawQuadsCount = quadsCount = usingTexturesCount = 0;
428         if(ppShader !is null)
429         {
430             fb.unbind();
431             draw(fbTexRegion, 0,0 );
432             draw();
433         }
434         lastDrawQuadsCount = quadsCount = usingTexturesCount = 0;
435     }
436 }